using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using MonoTorrent.BEncoding;
using MonoTorrent.Client;
using MonoTorrent.Client.PieceWriters;

namespace MonoTorrent.Common
{
	public class TorrentCreator
	{
		readonly List<List<string>> announces; // The list of announce urls
		readonly BEncodedDictionary dict; // The BencodedDictionary which contains the data to be written to the .torrent file
		SHA1 hasher = HashAlgoFactory.Create<SHA1>();
		bool ignoreHiddenFiles; // True if you want to ignore hidden files when making the torrent
		string path = ""; // The path from which the torrent will be created (can be file or directory)
		TorrentCreatorAsyncResult result; // The IAsyncResult generated by creating the torrent

		public List<List<string>> Announces { get { return announces; } }

		public string Comment
		{
			get
			{
				var val = Get(dict, new BEncodedString("comment"));
				return val == null ? string.Empty : val.ToString();
			}
			set { Set(dict, "comment", new BEncodedString(value)); }
		}

		public string CreatedBy
		{
			get
			{
				var val = Get(dict, new BEncodedString("created by"));
				return val == null ? string.Empty : val.ToString();
			}
			set { Set(dict, "created by", new BEncodedString(value)); }
		}

		public string Encoding { get { return Get(dict, (BEncodedString)"encoding").ToString(); } private set { Set(dict, "encoding", (BEncodedString)value); } }

		internal SHA1 Hasher { get { return hasher; } set { hasher = value; } }

		public bool IgnoreHiddenFiles { get { return ignoreHiddenFiles; } set { ignoreHiddenFiles = value; } }

		/// <summary>
		/// The path from which the torrent can be created. This can be either
		/// a file or a folder containing the files to hash.
		/// </summary>
		public string Path { get { return path ?? ""; } set { path = value ?? ""; } }

		///<summary>
		/// The length of each piece in bytes (range 16384 bytes -> 4MB)
		///</summary>
		public long PieceLength
		{
			get
			{
				var val = Get((BEncodedDictionary)dict["info"], new BEncodedString("piece length"));
				return val == null ? -1 : ((BEncodedNumber)val).Number;
			}
			set { Set((BEncodedDictionary)dict["info"], "piece length", new BEncodedNumber(value)); }
		}

		///<summary>
		/// A private torrent can only accept peers from the tracker and will not share peer data
		/// through DHT
		///</summary>
		public bool Private
		{
			get
			{
				var val = Get((BEncodedDictionary)dict["info"], new BEncodedString("private"));
				return val == null ? false : ((BEncodedNumber)val).Number == 1;
			}
			set { Set((BEncodedDictionary)dict["info"], "private", new BEncodedNumber(value ? 1 : 0)); }
		}

		public string Publisher
		{
			get
			{
				var val = Get((BEncodedDictionary)dict["info"], new BEncodedString("publisher"));
				return val == null ? string.Empty : val.ToString();
			}
			set { Set((BEncodedDictionary)dict["info"], "publisher", new BEncodedString(value)); }
		}

		public string PublisherUrl
		{
			get
			{
				var val = Get((BEncodedDictionary)dict["info"], new BEncodedString("publisher-url"));
				return val == null ? string.Empty : val.ToString();
			}
			set { Set((BEncodedDictionary)dict["info"], "publisher-url", new BEncodedString(value)); }
		}

		public bool StoreMD5 { get; set; }
		/// <summary>
		/// This event indicates the progress of the torrent creation and is fired every time a piece is hashed
		/// </summary>
		public event EventHandler<TorrentCreatorEventArgs> Hashed;

		public TorrentCreator()
		{
			var info = new BEncodedDictionary();
			announces = new List<List<string>>();
			ignoreHiddenFiles = true;
			dict = new BEncodedDictionary();
			dict.Add("info", info);

			// Add in initial values for some of the torrent attributes
			PieceLength = 256*1024; // 256kB default piece size
			Encoding = "UTF-8";
		}

		/// <summary>
		/// Adds a custom value to the main bencoded dictionary
		/// </summary>        
		public void AddCustom(BEncodedString key, BEncodedValue value)
		{
			dict.Add(key, value);
		}

		public TorrentCreatorAsyncResult BeginCreate(object asyncState, AsyncCallback callback)
		{
			if (result != null) throw new TorrentException("You must call EndCreate before calling BeginCreate again");

			result = new TorrentCreatorAsyncResult(callback, asyncState);

			// Start the processing in a seperate thread, a user thread.
			new Thread(new ThreadStart(AsyncCreate)).Start();
			return result;
		}

		/// <summary>
		/// Creates a Torrent and returns it in it's dictionary form
		/// </summary>
		/// <returns></returns>
		public BEncodedDictionary Create()
		{
			return Create(new DiskWriter());
		}

		internal BEncodedDictionary Create(PieceWriter writer)
		{
			if (!Directory.Exists(Path) && !File.Exists(Path)) throw new ArgumentException("no such file or directory", Path);

			var files = new List<TorrentFile>();
			LoadFiles(Path, files);

			if (files.Count == 0) throw new TorrentException("There were no files in the specified directory");

			var parts = path.Split(new char[] { System.IO.Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries);
			var name = File.Exists(Path) ? System.IO.Path.GetFileName(path) : parts[parts.Length - 1];
			return Create(files.ToArray(), writer, name);
		}

		///<summary>
		/// Creates a Torrent and writes it to disk in the specified location
		///<summary>
		///<param name="storagePath">The path (including filename) where the new Torrent will be written to</param>
		public void Create(string path)
		{
			Check.PathNotEmpty(path);

			using (var stream = new FileStream(path, FileMode.Create)) Create(stream);
		}

		/// <summary>
		/// Generates a Torrent and writes it to the supplied stream
		/// </summary>
		/// <param name="stream">The stream to write the torrent to</param>
		public void Create(Stream stream)
		{
			Check.Stream(stream);

			var torrentDict = Create();

			var data = torrentDict.Encode();
			stream.Write(data, 0, data.Length);
		}

		internal BEncodedDictionary Create(TorrentFile[] files, PieceWriter writer, string name)
		{
			// Clone the base dictionary and fill the remaining data into the clone
			var torrentDict = BEncodedDictionary.Decode<BEncodedDictionary>(dict.Encode());
			Array.Sort<TorrentFile>(files, delegate(TorrentFile a, TorrentFile b) { return String.CompareOrdinal(a.Path, b.Path); });

			if (Directory.Exists(Path))
			{
				Logger.Log(null, "Creating multifile torrent from: {0}", Path);
				CreateMultiFileTorrent(torrentDict, files, writer, name);
			}
			else
			{
				Logger.Log(null, "Creating singlefile torrent from: {0}", Path);
				CreateSingleFileTorrent(torrentDict, files, writer, name);
			}

			return torrentDict;
		}

		/// <summary>
		/// Ends the asynchronous torrent creation and returns the torrent in it's dictionary form
		/// </summary>
		/// <param name="result"></param>
		/// <returns></returns>
		public BEncodedDictionary EndCreate(IAsyncResult result)
		{
			Check.Result(result);

			if (result != this.result) throw new ArgumentException("The supplied async result does not correspond to currently active async result");

			try
			{
				if (!result.IsCompleted) result.AsyncWaitHandle.WaitOne();

				if (this.result.SavedException != null) throw this.result.SavedException;

				return this.result.Aborted ? null : this.result.Dictionary;
			}
			finally
			{
				this.result = null;
			}
		}

		/// <summary>
		/// Ends the asynchronous torrent creation and writes the torrent to the specified path
		/// </summary>
		/// <param name="result"></param>
		/// <returns></returns>
		public bool EndCreate(IAsyncResult result, string path)
		{
			if (string.IsNullOrEmpty(path)) throw new ArgumentNullException("path");

			using (var s = File.OpenWrite(path)) return EndCreate(result, s);
		}

		/// <summary>
		/// Ends the asynchronous torrent creation and writes the torrent to the specified stream
		/// </summary>
		/// <param name="result"></param>
		/// <returns></returns>
		public bool EndCreate(IAsyncResult result, Stream stream)
		{
			if (stream == null) throw new ArgumentNullException("stream");

			var data = EndCreate(result);
			var buffer = data.Encode();
			if (data != null) stream.Write(buffer, 0, buffer.Length);

			return data != null;
		}

		///<summary>
		/// Calculates the approximate size of the final .torrent in bytes
		///</summary>
		public long GetSize()
		{
			var paths = new MonoTorrentCollection<string>();

			if (Directory.Exists(path)) GetAllFilePaths(path, paths);
			else if (File.Exists(path)) paths.Add(path);
			else return 64*1024;

			long size = 0;
			for (var i = 0; i < paths.Count; i++) size += new FileInfo(paths[i]).Length;

			return size;
		}

		public int RecommendedPieceSize()
		{
			var totalSize = GetSize();

			// Check all piece sizes that are multiples of 32kB 
			for (var i = 32768; i < 4*1024*1024; i *= 2)
			{
				var pieces = (int)(totalSize/i) + 1;
				if ((pieces*20) < (60*1024)) return i;
			}

			// If we get here, we're hashing a massive file, so lets limit
			// to a max of 4MB pieces.
			return 4*1024*1024;
		}

		/// <summary>
		/// Removes a custom value from the main bencoded dictionary.
		/// </summary>
		public void RemoveCustom(BEncodedString key)
		{
			dict.Remove(key);
		}

		///<summary>
		///this adds stuff common to single and multi file torrents
		///</summary>
		void AddCommonStuff(BEncodedDictionary torrent)
		{
			if (announces.Count > 0 && announces[0].Count > 0) torrent.Add("announce", new BEncodedString(announces[0][0]));

			// If there is more than one tier or the first tier has more than 1 tracker
			if (announces.Count > 1 || (announces.Count > 0 && announces[0].Count > 1))
			{
				var announceList = new BEncodedList();
				for (var i = 0; i < announces.Count; i++)
				{
					var tier = new BEncodedList();
					for (var j = 0; j < announces[i].Count; j++) tier.Add(new BEncodedString(announces[i][j]));

					announceList.Add(tier);
				}

				torrent.Add("announce-list", announceList);
			}

			var epocheStart = new DateTime(1970, 1, 1);
			var span = DateTime.Now - epocheStart;
			Logger.Log(null, "creation date: {0} - {1} = {2}:{3}", DateTime.Now, epocheStart, span, span.TotalSeconds);
			torrent.Add("creation date", new BEncodedNumber((long)span.TotalSeconds));
		}

		///<summary>calculate md5sum of a file</summary>
		///<param name="fileName">the file to sum with md5</param>
		void AddMD5(BEncodedDictionary dict, string fileName)
		{
			var hasher = MD5.Create();
			var sb = new StringBuilder();

			using (var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read))
			{
				var hash = hasher.ComputeHash(stream);

				foreach (var b in hash)
				{
					var hex = b.ToString("X");
					hex = hex.Length > 1 ? hex : "0" + hex;
					sb.Append(hex);
				}
				Logger.Log(null, "Sum for: '{0}' = {1}", fileName, sb.ToString());
			}
			dict.Add("md5sum", new BEncodedString(sb.ToString()));
		}

		void AsyncCreate()
		{
			try
			{
				result.Dictionary = Create();
			}
			catch (Exception ex)
			{
				result.SavedException = ex;
			}

			result.IsCompleted = true;
			result.AsyncWaitHandle.Set();

			if (result.Callback != null) result.Callback(result);
		}

		///<summary>
		///calculates all hashes over the files which should be included in the torrent
		///</summmary>
		byte[] CalcPiecesHash(string path, TorrentFile[] files, PieceWriter writer)
		{
			var piecesBuffer = new byte[GetPieceCount(files)*20]; //holds all the pieces hashes
			var piecesBufferOffset = 0;

			var totalLength = Toolbox.Accumulate<TorrentFile>(files, delegate(TorrentFile f) { return f.Length; });
			var buffer = new ArraySegment<byte>(new byte[PieceLength]);

			while (totalLength > 0)
			{
				var bytesToRead = (int)Math.Min(totalLength, PieceLength);
				var io = new BufferedIO(null, buffer, (piecesBufferOffset/20)*PieceLength, bytesToRead, bytesToRead, files, path);
				totalLength -= writer.ReadChunk(io);

				// If we are using the synchronous version, result is null
				if (result != null && result.Aborted) return piecesBuffer;

				var currentHash = hasher.ComputeHash(buffer.Array, 0, io.ActualCount);
				RaiseHashed(new TorrentCreatorEventArgs(0,
				                                        0,
				                                        //reader.CurrentFile.Position, reader.CurrentFile.Length,
				                                        piecesBufferOffset*PieceLength,
				                                        (piecesBuffer.Length - 20)*PieceLength));
				Buffer.BlockCopy(currentHash, 0, piecesBuffer, piecesBufferOffset, currentHash.Length);
				piecesBufferOffset += currentHash.Length;
			}
			return piecesBuffer;
		}

		///<summary>
		///used for creating multi file mode torrents.
		///</summary>
		///<returns>the dictionary representing which is stored in the torrent file</returns>
		protected void CreateMultiFileTorrent(BEncodedDictionary dictionary, TorrentFile[] files, PieceWriter writer, string name)
		{
			AddCommonStuff(dictionary);
			var info = (BEncodedDictionary)dictionary["info"];

			var torrentFiles = new BEncodedList(); //the dict which hold the file infos

			for (var i = 0; i < files.Length; i++) torrentFiles.Add(GetFileInfoDict(files[i]));

			info.Add("files", torrentFiles);

			Logger.Log(null, "Topmost directory: {0}", name);
			info.Add("name", new BEncodedString(name));

			info.Add("pieces", new BEncodedString(CalcPiecesHash(Path, files, writer)));
		}

		///<summary>
		///used for creating a single file torrent file
		///<summary>
		///<returns>the dictionary representing which is stored in the torrent file</returns>
		protected void CreateSingleFileTorrent(BEncodedDictionary dictionary, TorrentFile[] files, PieceWriter writer, string name)
		{
			AddCommonStuff(dictionary);

			var infoDict = (BEncodedDictionary)dictionary["info"];
			infoDict.Add("length", new BEncodedNumber(files[0].Length));
			infoDict.Add("name", (BEncodedString)name);

			if (StoreMD5) AddMD5(infoDict, Path);

			Logger.Log(null, "name == {0}", name);
			var path = System.IO.Path.GetDirectoryName(Path);
			infoDict.Add("pieces", new BEncodedString(CalcPiecesHash(path, files, writer)));
		}

		static BEncodedValue Get(BEncodedDictionary dictionary, BEncodedString key)
		{
			return dictionary.ContainsKey(key) ? dictionary[key] : null;
		}

		void GetAllFilePaths(string directory, MonoTorrentCollection<string> paths)
		{
			var subs = Directory.GetDirectories(directory);
			foreach (var path in subs)
			{
				if (ignoreHiddenFiles)
				{
					var info = new DirectoryInfo(path);
					if ((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) continue;
				}

				GetAllFilePaths(path, paths);
			}

			subs = Directory.GetFiles(directory);
			foreach (var path in subs)
			{
				if (ignoreHiddenFiles)
				{
					var info = new FileInfo(path);
					if ((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) continue;
				}

				paths.Add(path);
			}
		}

		///<summary>
		///this method is used for multi file mode torrents to return a dictionary with
		///file relevant informations. 
		///<param name="file">the file to report the informations for</param>
		///<param name="basePath">used to subtract the absolut path information</param>
		///</summary>
		BEncodedDictionary GetFileInfoDict(TorrentFile file)
		{
			var fileDict = new BEncodedDictionary();

			fileDict.Add("length", new BEncodedNumber(file.Length));

			var filePath = new BEncodedList();
			var splittetPath = file.Path.Split(System.IO.Path.DirectorySeparatorChar);

			foreach (var s in splittetPath)
			{
				if (s.Length > 0) //exclude empties
					filePath.Add(new BEncodedString(s));
			}

			fileDict.Add("path", filePath);

			return fileDict;
		}

		long GetPieceCount(TorrentFile[] files)
		{
			long size = 0;
			foreach (var file in files) size += file.Length;

			//double count = (double)size/PieceLength;
			var pieceCount = size/PieceLength + (((size%PieceLength) != 0) ? 1 : 0);
			Logger.Log(null, "Piece Count: {0}", pieceCount);
			return pieceCount;
		}

		void LoadFiles(string path, List<TorrentFile> files)
		{
			if (Directory.Exists(path)) foreach (var subdir in Directory.GetFileSystemEntries(path)) LoadFiles(subdir, files);
			else if (File.Exists(path))
			{
				string filePath;
				if (path == Path) filePath = System.IO.Path.GetFileName(path);
				else
				{
					filePath = path.Substring(Path.Length);
					if (filePath[0] == System.IO.Path.DirectorySeparatorChar) filePath = filePath.Substring(1);
				}
				var info = new FileInfo(path);
				if (!((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden && IgnoreHiddenFiles)) files.Add(new TorrentFile(filePath, info.Length, 0, 0, null, null, null));
			}
		}

		void RaiseHashed(TorrentCreatorEventArgs e)
		{
			Toolbox.RaiseAsyncEvent<TorrentCreatorEventArgs>(Hashed, this, e);
		}

		static void Set(BEncodedDictionary dictionary, BEncodedString key, BEncodedValue value)
		{
			if (dictionary.ContainsKey(key)) dictionary[key] = value;
			else dictionary.Add(key, value);
		}
	}
}